package com.atguigu.paymentdemo.service.impl;

import com.atguigu.paymentdemo.config.WxPayConfig;
import com.atguigu.paymentdemo.entity.OrderInfo;
import com.atguigu.paymentdemo.enums.OrderStatus;
import com.atguigu.paymentdemo.enums.wxpay.WxApiType;
import com.atguigu.paymentdemo.enums.wxpay.WxNotifyType;
import com.atguigu.paymentdemo.enums.wxpay.WxTradeState;
import com.atguigu.paymentdemo.service.OrderInfoService;
import com.atguigu.paymentdemo.service.PaymentInfoService;
import com.atguigu.paymentdemo.service.RefundInfoService;
import com.atguigu.paymentdemo.service.WxPayService;
import com.atguigu.paymentdemo.uitl.Distributed.RedisDistributedLock;
import com.atguigu.paymentdemo.uitl.HttpUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;


@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {
    @Resource
    OrderInfoService orderInfoService;
    @Resource
    PaymentInfoService paymentInfoService;

    @Resource
    Gson gson;
    @Resource
    WxPayConfig wxPayConfig;

    @Resource
    HttpClient httpClient;

    @Resource
    RefundInfoService refundInfoService;

    @Resource
    RedisDistributedLock distributedLock;

    //回调信息并发控制
    private final ReentrantLock lock =  new ReentrantLock();
    //回调退款信息并发控制
    private final ReentrantLock refundLock =  new ReentrantLock();

    final String codeUrl = "codeUrl";


    /**
     *  创建订单，返回 二维码字符串和订单号 map
     * @param productId
     * @param userId
     * @return
     * @throws Exception
     */

    public Map<String, String> nativePay(Long productId, Long userId) throws Exception{
        OrderInfo orderInfo;
        Map<String, String> res = new HashMap<>();
        //先查看该用户是否 有该 productId 未支付的订单
        orderInfo = getNoPayOrderInfo(productId,userId);

        if(orderInfo != null){

            //前端获取的是"codeUrl"不是"code_url"，因此需要改名
            res.put("codeUrl", orderInfo.getCodeUrl());
            res.put("orderNo", orderInfo.getOrderNo());
            return res;
        }
        //通过请求商品ID生成订单实体
        orderInfo = orderInfoService.orderInfoGenerate(productId,userId);

        //通过订单实体调用 下单API，返回返回二维码和订单号
        res = createCodeUrlandOrderNo(orderInfo);

        //获取二维码缓存到订单中
        orderInfo.setCodeUrl(res.get(codeUrl));

        //将订单存储到数据库，也可以用orderInfoMapper
        orderInfoService.save(orderInfo);

        return res;

    }

    /**
     * 调用下单API,返回CodeURL和OrderNumber
     * @throws Exception
     */

    public Map<String,String> createCodeUrlandOrderNo(OrderInfo orderInfo) throws Exception{
        //创建远程请求对象
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain()+ WxApiType.NATIVE_PAY.getType());

        // 生成请求body相关参数
        String reqdata = ParamMapGeneration(orderInfo);

        //请求体封装 请求体填充 相关参数设置
        httpPost = httpPostPackage(httpPost,reqdata);

        //完成签名并执行请求
        CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());//响应体 -- {"code_url":"weixin://wxpay/bizpayurl?pr=6ivLWpKzz"}

            Map<String,String> res = gson.fromJson(bodyAsString,HashMap.class);

            int statusCode = response.getStatusLine().getStatusCode();

            statusCodeResponse(statusCode,bodyAsString);

            String codeUrl = res.get("code_url");


            Map<String, String> map = new HashMap<>();
            //前端获取的是"codeUrl"不是"code_url"，因此需要改名
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());

            return map;

        } finally {
            response.close();
        }

    }


    /**
     * 封装支付请求的相关参数
     * @param orderInfo
     * @return json
     */
    public String ParamMapGeneration(OrderInfo orderInfo){

        // 请求body参数
        Map<String,Object> paramMap = new HashMap<>();

        paramMap.put("appid",wxPayConfig.getAppid());
        paramMap.put("mchid",wxPayConfig.getMchId());

        paramMap.put("out_trade_no",orderInfo.getOrderNo());
        paramMap.put("description",orderInfo.getTitle());
        paramMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
        //paramMap.put("total",orderInfo.getTotalFee());

        Map<String,Object> amountMap = new HashMap();
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");

        paramMap.put("amount",amountMap);

        return gson.toJson(paramMap);

    }


    /**
     * 查看是否有未支付的订单
     * 用于防止用户订单信息重复提交
     * 如果数据库中有该用户未支付的订单直接返回该订单，无需重复创建
     */

    public OrderInfo getNoPayOrderInfo(Long productId, Long userId){
        QueryWrapper<OrderInfo> wrapper = new QueryWrapper<>();

        wrapper.eq("product_id",productId);
        wrapper.eq("user_id",userId);
        wrapper.eq("order_status",OrderStatus.NOTPAY.getType());
        OrderInfo orderInfo = orderInfoService.getOne(wrapper);

        return orderInfo;

    }

    /**
     *回调通知后进行订单处理
     * 分布式锁解决并发问题
     * @param resource
     * @throws GeneralSecurityException
     */

    public void processOrder(Map<String,String> resource) throws GeneralSecurityException {
        log.info("订单处理");
        String plaintText =decrypt(resource);
        Map<String,String> notifyMeaasge = gson.fromJson(plaintText, HashMap.class);
        String trade_no = notifyMeaasge.get("out_trade_no");


        //过滤平台回调的重复请求
        //接口调用的幂等性，无论接口被调用多少次，产生的结果是一样的

        //尝试获取锁：成功获取则立即返回true，获取失败则立即返回false，不必一直等待锁的释放
        log.info("尝试获取锁");
        if(distributedLock.lock(trade_no)){
            try{
                log.info("获取锁成功");

                if(paymentInfoService.isExistPaymentInfo(trade_no)) {
                    log.info("过滤来自平台的已处理请求");
                    return;
                }
                //模拟回调并发进入product_id
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //订单信息更新
                orderInfoService.ChangeTradeState(trade_no, OrderStatus.SUCCESS.getType());
                //支付信息更新
                paymentInfoService.createPaymentInfo(plaintText);

            }finally {
                if(distributedLock.unlock(trade_no)) log.info("解锁成功");
            }

        }else{
            log.info("尝试获取锁失败");
        }
    }

    /**
     *回调通知后进行订单处理
     * ReentryLock解决并发问题
     * @param resource
     * @throws GeneralSecurityException
     */

    public void processOrder_adandon(Map<String,String> resource) throws GeneralSecurityException {
        log.info("订单处理");
        String plaintText =decrypt(resource);
        Map<String,String> notifyMeaasge = gson.fromJson(plaintText, HashMap.class);
        String trade_no = notifyMeaasge.get("out_trade_no");


        //过滤平台回调的重复请求
        //接口调用的幂等性，无论接口被调用多少次，产生的结果是一样的

        //尝试获取锁：成功获取则立即返回true，获取失败则立即返回false，不必一直等待锁的释放
        log.info("尝试获取锁");
        if(lock.tryLock()){
            log.info("获取锁成功");
            try{
                if(paymentInfoService.isExistPaymentInfo(trade_no)) {
                    log.info("过滤来自平台的已处理请求");
                    return;
                }

                //模拟回调并发进入product_id
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //订单信息更新
                orderInfoService.ChangeTradeState(trade_no, OrderStatus.SUCCESS.getType());
                //支付信息更新
                paymentInfoService.createPaymentInfo(plaintText);

            }finally {
                lock.unlock();

            }
        }else{
            log.info("尝试获取锁失败");
        }
    }



    /**
     * 对称解密
     * @param resource
     * @return
     * @throws GeneralSecurityException
     */
    public String decrypt(Map<String,String> resource) throws GeneralSecurityException {
        log.info("对称解密开始....");
        String nonce =  resource.get("nonce");
        String associated_data = resource.get("associated_data");
        String ciphertext = resource.get("ciphertext");

        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));

        String plaintText = aesUtil.decryptToString(associated_data.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
        log.info("加密密文："+ciphertext);
        log.info("解密明文："+plaintText);

        return plaintText;
    }

    /**
     * 订单取消
     * @param orderNo
     */
    public void cancelOrder(String orderNo) throws IOException {
        log.info("关单：" + orderNo);
        //微信平台关单
        wxPayCancelOrder(orderNo);
        //数据库状态修改
        orderInfoService.ChangeTradeState(orderNo,OrderStatus.CANCEL.getType());

    }

    /**
     * 调用微信关单API 关单
     * @param orderNo
     */

    public void wxPayCancelOrder(String orderNo) throws IOException {
        //创建远程请求对象
        String url =wxPayConfig.getDomain() + String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo);
        HttpPost httpPost = new HttpPost(url);

        // 生成请求body相关参数
        Map<String,String> paramMap = new HashMap<>();
        paramMap.put("mchid",wxPayConfig.getMchId());
        String reqdata = gson.toJson(paramMap);

        //将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(reqdata,"utf-8");
        //设置请求体 为json格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        //设置返回的响应体 为json格式
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpPost);

        try {
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 204) { //处理成功，无返回Body
                log.info("订单"+ orderNo + "关单成功");
            } else {
                log.info("订单"+ orderNo + "关单失败");
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
    }


    /**
     * 对于超时未支付的订单准备关单
     * 调用查单接口核实订单状态
     * 如果已支付，则更新商户端订单状态
     * 如果未支付，则调用关单接口，并更新商户端订单接口
     * @param orderInfo
     */

    public void checkOrderStatus(OrderInfo orderInfo) throws IOException {
        log.warn("根据订单号核实订单状态：" + orderInfo.getOrderNo());
        //调用查单接口核实订单状态
        String bodyAsString = wxPayQueryOrder(orderInfo.getOrderNo());
        //订单处理
        // * 如果已支付，则更新商户端订单状态
        // * 如果未支付，则调用关单接口，并更新商户端订单接口
        processOrder(bodyAsString);

    }


    /**未收到回调消息时，调用微信查单API 查询订单情况
     * path指该参数为路径参数
     * query指该参数需在请求URL传参
     * body指该参数需在请求JSON传参
     * @param orderNo
     * @throws IOException
     */

    public String wxPayQueryOrder(String orderNo) throws IOException {

        //创建远程请求对象
        String url =wxPayConfig.getDomain() + String.format(WxApiType.ORDER_QUERY_BY_NO.getType(),orderNo) + "?mchid="+wxPayConfig.getMchId();
        HttpGet httpGet = new HttpGet(url);

        //设置返回的响应体 为json格式
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpGet);

        String bodyAsString = null;

        try {
            bodyAsString = EntityUtils.toString(response.getEntity());//响应体
            log.info("查单返回结果 ： " + bodyAsString);

            int statusCode = response.getStatusLine().getStatusCode();

            statusCodeResponse(statusCode,bodyAsString);
            //修改订单信息 创建支付日志信息
            //processOrder(bodyAsString);

        } finally {
            response.close();
        }
        return bodyAsString;

    }


    /**
     *主动查单后，对来自微信平台的通知后进行订单处理
     *      * 如果已支付，则更新商户端订单状态
     *      * 如果未支付，则调用关单接口，并更新商户端订单接口
     * @param plaintText
     * @throws GeneralSecurityException
     */

    public void processOrder(String plaintText) throws IOException {
        log.info("订单处理");
        Map<String,String> notifyMeaasge = gson.fromJson(plaintText, HashMap.class);
        String trade_no = notifyMeaasge.get("out_trade_no");
        String tradeState = notifyMeaasge.get("trade_state");

        //接口调用的幂等性，无论接口被调用多少次，产生的结果是一样的
        if(paymentInfoService.isExistPaymentInfo(trade_no)) {
            log.info("过滤来自平台的已处理响应");
            return;
        }
        //如果已支付，则更新商户端订单状态
        if(tradeState.equals(WxTradeState.SUCCESS.getType())){
            log.info("支付成功："+trade_no);
            orderInfoService.ChangeTradeState(trade_no,OrderStatus.SUCCESS.getType());

        }
        //如果未支付，则调用关单接口,无需要记录支付日志
        if(tradeState.equals(WxTradeState.NOTPAY.getType())){
            log.info("未支付："+trade_no);
            //微信平台关单
            log.info("微信平台关单："+trade_no);
            wxPayCancelOrder(trade_no);
            //数据库状态修改
            log.info("数据库状态修改：" +trade_no +" 订单 " +OrderStatus.CLOSED.getType());
            orderInfoService.ChangeTradeState(trade_no,OrderStatus.CLOSED.getType());

        }

    }

    /**
     * 退款
     * @param orderNo
     * @param reason
     */


    public void orderRefund(String orderNo, String reason) throws IOException {
        log.info("创建退款单记录");
        //根据订单编号，生成初始退款日志并修改订单状态为退款中
        orderInfoService.ChangeTradeState(orderNo,OrderStatus.REFUND_PROCESSING.getType());
        refundInfoService.createRefundInfo(orderNo, reason);

        //调用微信退款API
        log.info("调用退款单API");
        String bodyAsString = wxPayRefund(orderNo,reason);
        log.info("退款响应body：");
        log.info(bodyAsString);
        //更新退款订单信息
        log.info("接受退款结果返回参数更新日志："+ orderNo);
        refundInfoService.updateRefundInfo(bodyAsString,orderNo);
    }

    /**
     * 调用微信退款API
     */
    public String wxPayRefund(String orderNo, String resaon) throws IOException {
        //根据微信平台接口创建URL
        String url = wxPayConfig.getDomain() + WxApiType.DOMESTIC_REFUNDS.getType();
        //创建httpPost对象
        HttpPost httpPost = new HttpPost(url);

        String bodyParams = refundParamMapGeneration(orderNo,resaon);
        //httpPost 请求体封装与 相关参数设置
        httpPost = httpPostPackage(httpPost,bodyParams);

        //签名 请求 获取响应  验签
        CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            statusCodeResponse(statusCode,bodyAsString);

        } finally {
            response.close();
        }
        return bodyParams;

    }


    /**
     * 生成退款请求参数bodyParams
     * @return
     */
    public String refundParamMapGeneration(String orderNo, String resaon){
        //创建bodyMap
        Map<String,Object> bodyMap = new HashMap<>();
        bodyMap.put("out_trade_no",orderNo);
        bodyMap.put("out_refund_no",orderNo);
        bodyMap.put("reason",resaon);
        bodyMap.put("notify_url",wxPayConfig.getNotifyDomain()+WxNotifyType.REFUND_NOTIFY.getType());


        Map<String,Object> amount = new HashMap<>();
        Integer totalFee = orderInfoService.queryOrderTotalFeeByOrderNo(orderNo);
        amount.put("refund",totalFee);
        amount.put("total",totalFee);
        amount.put("currency","CNY");

        bodyMap.put("amount",amount);

        //通过bodyMap生成请求参数bodyParams
        String bodyParams= gson.toJson(bodyMap);
        return bodyParams;
    }

    /**
     *退款回调通知后进行退款订单处理
     * @param resource
     * @throws GeneralSecurityException
     */

    public void processRefundOrder(Map<String,String> resource) throws GeneralSecurityException {
        log.info("退款回调通知订单处理");
        String plaintText =decrypt(resource);
        Map<String,String> notifyMeaasge = gson.fromJson(plaintText, HashMap.class);
        String trade_no = notifyMeaasge.get("out_trade_no");

        //过滤平台回调的重复请求
        //接口调用的幂等性，无论接口被调用多少次，产生的结果是一样的
        //尝试获取锁：成功获取则立即返回true，获取失败则立即返回false，不必一直等待锁的释放
        log.info("尝试获取退单锁");
        if(refundLock .tryLock()){
            log.info("尝试获取退单锁成功");
            try{
                if(refundInfoService.isProcessedRefundInfo(trade_no)){
                    log.info("过滤来自平台的已处理退款请求");
                    return;
                }

                //模拟回调并发进入product_id
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("回调后，订单和退款信息更新");

                //订单信息更新
                orderInfoService.ChangeTradeState(trade_no, OrderStatus.REFUND_SUCCESS.getType());
                //refuninfo更新
                refundInfoService.updateRefundInfoByNotify(plaintText,trade_no);

            }finally {
                refundLock.unlock();
            }
        }else{
            log.info("尝试获取退单锁失败");
        }
    }



    /**
     * httpPost 请求体封装与 相关参数设置
     * @param httpPost
     * @param
     * @return
     */

    public HttpPost httpPostPackage(HttpPost httpPost,String bodyParams){

        //封装成请求体
        StringEntity entity = new StringEntity(bodyParams,"utf-8");
        entity.setContentType("application/json");//设置请求报文格式
        httpPost.setEntity(entity);//将请求报文放入请求对象
        httpPost.setHeader("Accept","application/json");//设置响应报文格式
        return httpPost;
    }

    /**
     * 状态码响应
     * @param
     * @return
     */

    public void statusCodeResponse(int statusCode,String bodyAsString) throws IOException {


        if (statusCode == 200) { //处理成功
            System.out.println("success,return body = " + bodyAsString);

        } else if (statusCode == 204) { //处理成功，无返回Body
            System.out.println("success");
        } else {
            System.out.println("failed,resp code = " + statusCode+ ",return body = " + bodyAsString);
            throw new IOException("request failed");
        }

    }




}
